#define _DEFAULT_SOURCE

#include <pwd.h>
#include <grp.h>
#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <limits.h>
#include <unistd.h>
#include <inttypes.h>

#include "../util.h"
#include "../getopt_helper.h"

static struct option const LONG_OPTIONS[] = {
	{ "context", no_argument, NULL, 'Z' },
	{ "group", no_argument, NULL, 'g' },
	{ "groups", no_argument, NULL, 'G' },
	{ "name", no_argument, NULL, 'n' },
	{ "real", no_argument, NULL, 'r' },
	{ "user", no_argument, NULL, 'u' },
	{ "zero", no_argument, NULL, 'z' },

	GETOPT_END_DECL
};
static struct option const SHORT_LONG_OPTIONS[] = { GETOPT_END_DECL };

static char whoami_usage[] =
    "Usage: %s [OPTION]...\n"
    "Print the user name associated with the current effective user ID.\n"
    "Same as " PROG_NAME
    " -un.\n"
    "\n"
    "      --help     display this help and exit\n"
    "      --version  output version information and exit";

static char groups_usage[] =
    "Usage: %s [OPTION]... [USER]\n"
    "Print current group names for specified USER,\n"
    "or (when USER omitted) for the current user\n"
    "Same as " PROG_NAME
    " -Gn.\n"
    "\n"
    "      --help     display this help and exit\n"
    "      --version  output version information and exit";

static char id_usage[] =
    "Usage: %s [OPTION]... [USER]...\n"
    "Print user and group information for each specified USER,\n"
    "or (when USER omitted) for the current user.\n"
    "\n"
    "  -a             ignore, for compatibility with other versions\n"
    "  -Z, --context  print only the security context of the process\n"
    "  -g, --group    print only the effective group ID\n"
    "  -G, --groups   print all group IDs\n"
    "  -n, --name     print a name instead of a number, for -ugG\n"
    "  -r, --real     print the real ID instead of the effective ID, with "
    "-ugG\n"
    "  -u, --user     print only the effective user ID\n"
    "  -z, --zero     delimit entries with NUL characters, not whitespace;\n"
    "                   not permitted in default format\n"
    "      --help     display this help and exit\n"
    "      --version  output version information and exit\n"
    "\n"
    "Without any OPTION, print some useful set of identified information.";

struct passwd *who(const char *);
void	       group(gid_t);
void	       group_default(gid_t);
void	       groups(struct passwd *, void (*func)(gid_t));
void	       user(struct passwd *);

static enum flag INIT_FLAGS(NO, z, n);
static uint32_t	 space = 0;

int
main(int argc, char *argv[])
{
	enum {
		ID,
		GROUPS,
		WHOAMI,
	} prog;

	int		     ch;
	struct group	     *gr;
	struct passwd	      *pw = NULL;
	struct option const *long_opts;
	char		     *opts, *usage_str;
	enum flag	     INIT_FLAGS(NO, Z, g, G, r, u);

	if (STREQ(__progname, "whoami")) {
		prog  = WHOAMI;

		uflag = nflag = YES;

		opts	      = "";
		long_opts     = SHORT_LONG_OPTIONS;
		usage_str     = whoami_usage;
	} else if (STREQ(__progname, "groups")) {
		prog  = GROUPS;

		Gflag = nflag = YES;

		opts	      = "";
		long_opts     = SHORT_LONG_OPTIONS;
		usage_str     = groups_usage;
	} else {
		prog	  = ID;

		opts	  = "agnruzGZ";
		long_opts = LONG_OPTIONS;
		usage_str = id_usage;
	}

	while ((ch = getopt_long(argc, argv, opts, long_opts, NULL)) != -1) {
		switch (ch) {
			IS_FLAGS(YES, g, G, n, r, u, z);

		case 'a':
			/* Ignore -a, for compatibility with SVR4.  */
			break;

		case 'Z':
			Zflag = YES;
			(void)Zflag;
			errx(2, "%c: not implemented", ch);
			break;

			GETOPT_END_CASE
		}
	}

	if (uflag + gflag + Gflag + Zflag > 1)
		errx(!0, "cannot print 'only' of more than one choice");

	argv += optind;

	if (*argv) {
		switch (prog) {
		case GROUPS:
			if (argc - optind > 1) {
				argv++;
				goto err;
			}
			/* fallthrough */

		case ID:
			pw = who(*argv);
			break;

		case WHOAMI:
		err:
			errx(!0, "extra operand '%s'", *argv);
		}
	}

	/*
	 * to expand:
	 * clang -E -P - < id.c | clang-format - | bat -l c
	 */
#define gu_flag(GU, GRPW, L)                                                  \
	do {                                                                  \
		if (GU##flag) {                                               \
			GU##id_t _id =                                        \
			    pw ? pw->pw_##GU##id                              \
			       : (rflag ? get##GU##id() : gete##GU##id());    \
			if (nflag && ((GRPW) = get##GRPW##GU##id(_id)))       \
				puts((GRPW)->GRPW##_name);                    \
			else                                                  \
				printf("%u\n", _id);                          \
			return 0;                                             \
		}                                                             \
	} while (0)

	do {
		gu_flag(g, gr, endloop);
		gu_flag(u, pw, endloop);

		if (Gflag) {
			groups(pw, group);
			goto endloop;
		}

		user(pw);

	endloop:
		space = 0;
		if (zflag) putchar('\0');
		pw = *argv ? who(*argv) : NULL;
	} while (*argv && *++argv);

	return 0;
}

struct passwd *
who(const char *user)
{
	char	     *e;
	struct passwd    *pw;
	uid_t		  uid;
	unsigned long int val;

	if (!!(pw = getpwnam(user))) return pw;

	errno = 0;
	val   = strtoul(user, &e, 0);
	if (val > UINT_MAX) errno = ERANGE;
	SAFE_ASSIGN(uid, val);

	if (!errno && !*e && (pw = getpwuid(uid))) return pw;

	err(!0, "'%s': no such user", user);
}

void
group(gid_t gid)
{
	struct group *gr;

	if (space++) putchar(' ' * !zflag);

	if (nflag && (gr = getgrgid(gid)))
		fputs(gr->gr_name, stdout);
	else
		printf("%u", gid);
}

void
group_default(gid_t gid)
{
	struct group *gr;

	if (space++) putchar(',');

	printf("%u", gid);
	if ((gr = getgrgid(gid))) printf("(%s)", gr->gr_name);
}

void
groups(struct passwd *pw, void (*func)(gid_t))
{
	gid_t	 gid, *groups;
	int	 i, j, ngroups = NGROUPS_MAX;
	uint8_t *printed;

	if (!pw) {
		if ((ngroups = getgroups(0, NULL)) > 0)
			ngroups += 2; /* for current gid and egid */
		else
			err(!0, "getgroups(2)");
	}

#define alloc(V, S, T)	SAFE_ASSIGN(V, calloc((size_t)(S), sizeof(T)))
#define ralloc(V, S, T) SAFE_ASSIGN(V, realloc((V), (size_t)(S) * sizeof(T)))

	if (!alloc(groups, ngroups, gid_t)
	    || !alloc(printed, ngroups, uint8_t))
		NO_MEMORY();

	if (pw) {
		if (getgrouplist(pw->pw_name, pw->pw_gid, groups, &ngroups)
		    == -1) {
			if (!ralloc(groups, ngroups, gid_t)
			    || !ralloc(printed, ngroups, uint8_t))
				NO_MEMORY();
			else if (getgrouplist(pw->pw_name,
					      pw->pw_gid,
					      groups,
					      &ngroups)
				 == -1)
				err(!0, "too many groups");
		}
	} else {
		groups[0] = getegid();
		groups[1] = getgid();
		getgroups(ngroups - 2, groups + 2);
	}

	/* find uniq */
	for (i = 0; i < ngroups; i++)
		for (j = i + 1; j < ngroups; j++)
			if (groups[i] == groups[j]) printed[j] = !0;

	for (i = 0; i < ngroups; i++) {
		if (!printed[i]) {
			gid = groups[i];
			func(gid);
		}
	}

	free(groups);
	free(printed);

	putchar('\n' * !zflag);
}

void
user(struct passwd *pw)
{
#define print_uname(PW, ID)                                                   \
	do {                                                                  \
		if (((PW) = getpwuid(ID))) printf("(%s)", (PW)->pw_name);     \
	} while (0)
#define print_gname(GR, ID)                                                   \
	do {                                                                  \
		if (((GR) = getgrgid(ID))) printf("(%s)", (GR)->gr_name);     \
	} while (0)

	struct group *gr;
	enum flag     ispw = NO;
	uid_t	      uid, euid;
	gid_t	      gid, egid;

	if (pw) {
		ispw = YES;
		printf("uid=%u(%s) gid=%u",
		       pw->pw_uid,
		       pw->pw_name,
		       pw->pw_gid);
		print_gname(gr, pw->pw_gid);
	} else {
		uid = getuid();
		printf("uid=%u", uid);
		print_uname(pw, uid);
		if ((euid = geteuid()) != uid) {
			printf(" euid=%u", euid);
			print_uname(pw, euid);
		}

		gid = getgid();
		printf(" gid=%u", gid);
		print_gname(gr, gid);
		if ((egid = getegid()) != gid) {
			printf(" egid=%u", egid);
			print_gname(gr, egid);
		}
	}

	fputs(" groups=", stdout);
	groups(ispw ? pw : NULL, group_default);
}
